

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.table.*;

import java.util.Timer;
import java.util.TimerTask;
import java.util.List;
import java.util.ListIterator;


public class SearchGUI
{
    /* The preferred dimensions we'd like the content part of application 
     * to have.  None of the initial content panes provide a useful initial 
     * preferred size (e.g. the hitsTable and workersTable are empty) so 
     * we force the splashPane to be appWidth X appHeight.
     */
    private final static int appWidth = 640;
    private final static int appHeight = 480;


    private final static String hitsStatusPaneText = 
	"<html>" + 
	    "<table style=\"font-family:lucida; font-size:12\" border=0 width=100%>" + 
	      "<tr>" + 
		"<td style=\"padding:1\" align=left border=2 width=25%>Total Found: 0</td>" + 
		"<td border=0></td>" + 
		"<td style=\"padding:1\" align=left border=2 width=25%>Elapsed Time: 0</td>" + 
		"<td border=0 width=50%></td>" + 
	      "</tr>"+ 
	    "</table>" + 
	"</html>";


    private JMenuBar menuBar;
    private JPanel contentPane;
    private JTextField queryTextField;
    private JLabel nHitsLabel;
    private JTable hitsTable;
    private JTable workersTable;
    private CardPanel displayPane;
    private Icon splashIcon;
    private JComponent splashPane;
    private JEditorPane hitsStatusPane;
    private Timer heartbeatTimer = new Timer();
    private TimerTask doRequestUpdates = new DoRequestUpdates();
    private Search currentSearch = null;


    private void startSearch() {
	String query = queryTextField.getText().trim();
	currentSearch = new Search(query);
	currentSearch.start();
	resumeUpdates();
	showHits(currentSearch);
    }


    private void stopSearch(Search search) {
	if (search != null) {
	    search.stop();
	}
	suspendUpdates();
    }


    private void showHits(Search search) {
	if (search != null) {
	    hitsTable.setModel(search.getHitsTableModel());
	    displayPane.showCard("hitsTable");
	}
    }


    private void showWorkers(Search search) {
	if (search != null) {
	    workersTable.setModel(search.getWorkersTableModel());
	    displayPane.showCard("workersTable");
	}
    }


    /** 
     * Cancel the doRequestUpdates TimerTask and clear the updateNeeded property
     * on all of the workers.
     */
    private void suspendUpdates() {
	if (currentSearch != null) {
	    doRequestUpdates.cancel();
	    ListIterator workersItr = currentSearch.getWorkers().listIterator();
	    while(workersItr.hasNext()) {
		SearchWorker worker = (SearchWorker)(workersItr.next());
		worker.setUpdateNeeded(false);
	    }
	}
    }


    /** 
     * If any of the workers haven't finished yet, schedule a new 
     * doRequestUpdates timerTask.
     */
    private void resumeUpdates() {
	if (currentSearch != null) {
	    ListIterator workersItr = currentSearch.getWorkers().listIterator();
	    while(workersItr.hasNext()) {
		SearchWorker worker = (SearchWorker)(workersItr.next());
		switch(worker.getStatus()) {
		case SearchWorker.READY:
		case SearchWorker.RUNNING:
		    doRequestUpdates.cancel();
		    doRequestUpdates = new DoRequestUpdates();
		    heartbeatTimer.schedule(doRequestUpdates, 500, 1000);
		    return;
		}
	    }
	}
    }


    public void quit() {
	System.exit(0);
    }


    public SearchGUI() {
	splashIcon = new ImageIcon("images/splash.jpg");
    }


    public void init() 
    {

	splashPane = new JLabel(getSplashIcon());
	splashPane.setAlignmentX(0.5f);   // Used by OverlayLayout
	splashPane.setAlignmentY(0.5f);

	menuBar = new JMenuBar();
	contentPane = new JPanel(new BorderLayout());
	displayPane = new CardPanel();
	queryTextField = new JTextField();
	nHitsLabel = new JLabel();
	hitsTable = new JTable();
	workersTable = new JTable();

	hitsTable.setColumnSelectionAllowed(false);
	hitsTable.getTableHeader().addMouseListener(hitsTableHeaderMouseListener);

	JMenu fileMenu = new JMenu("File");
	fileMenu.add(stopSearchAction);
	fileMenu.add(quitAction);

	JMenu viewMenu = new JMenu("View");
	viewMenu.add(showHitsAction);
	viewMenu.add(showWorkersAction);

	Box searchBox = Box.createHorizontalBox();
	searchBox.add(new JLabel("Search the Web: "));
	searchBox.add(queryTextField);

	JPanel searchPane = new JPanel(new BorderLayout());
	searchPane.setBorder(BorderFactory.createEtchedBorder());
	searchPane.add(searchBox, BorderLayout.CENTER);

	JPanel splashBorder = new JPanel(null);
	splashBorder.setLayout(new OverlayLayout(splashBorder));
	int marginX = appWidth - splashIcon.getIconWidth();
	int marginY = appHeight - splashIcon.getIconHeight();
	int dx = Math.max(0, marginX) / 2;
	int dy = Math.max(0, marginY) / 2;
	splashBorder.setBorder(new EmptyBorder(dy, dx, marginY - dy, marginX - dx));
	splashBorder.setBackground(Color.black);
	splashBorder.add(splashPane);

	hitsStatusPane = new JEditorPane("text/html", hitsStatusPaneText);
	hitsStatusPane.setEditable(false);
	hitsStatusPane.setBackground(menuBar.getBackground());

	JPanel hitsPane = new JPanel(new BorderLayout());
	hitsPane.add(new JScrollPane(hitsTable), BorderLayout.CENTER);
	hitsPane.add(hitsStatusPane, BorderLayout.SOUTH);


	displayPane.add(splashBorder, "splash");
	displayPane.add(hitsPane, "hitsTable");
	displayPane.add(new JScrollPane(workersTable), "workersTable");

	
	menuBar.add(fileMenu);
	menuBar.add(viewMenu);

	contentPane.add(searchPane, BorderLayout.NORTH);
	contentPane.add(displayPane, BorderLayout.CENTER);

	queryTextField.setAction(startSearchAction);

	contentPane.getToolkit().getSystemEventQueue().push(new AppEventQueue());
    }


    /** 
     * Return a splash pane to display while the application starts up. 
     * This method is called by the application main(), before the 
     * rest of the GUI is initialized on a worker thread. 
     */
    public Icon getSplashIcon() {
	return splashIcon;
    }


    /** 
     * When this app comes up, we show the splashIcon inside this
     * container in the main frame.  See SearchPart.showApp().
     */
    public JComponent getSplashPane() {
	return splashPane;
    }

    public JComponent getContentPane() {
	return contentPane;
    }

    public JMenuBar getMenuBar() {
	return menuBar;
    }

    public String getTitle() {
	return "Search Party 0.0";
    }


    /* - Listeners, Actions, Runnables, TimerTasks, etc.. appear below - */


    /**
     * Sort a hitsTable column when the mouse is clicked in the columns 
     * header.  
     */
    private final MouseListener hitsTableHeaderMouseListener = new MouseAdapter() {
	public void mouseClicked(MouseEvent e) {
	    if (currentSearch == null) {
		return;
	    }
	    TableColumnModel columnModel = hitsTable.getColumnModel();
	    int viewColIndex = columnModel.getColumnIndexAtX(e.getX()); 
	    int modelColIndex = hitsTable.convertColumnIndexToModel(viewColIndex); 
	    if ((e.getClickCount() == 1) && (modelColIndex != -1)) {
		int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK; 
		boolean ascending = (shiftPressed == 0); 
		currentSearch.sortHits(modelColIndex, ascending); 
	    }
	}
    };


    /**
     * Each time this timer task runs, it sets the "updateNeeded" property
     * on all active SearchWorkers for the current Search.  If there are
     * aren't any active workers, e.g. because they're all finished, or
     * there's no current search, then the timer task cancels itself.
     * <p>
     * This task also updates the elapsed time display.
     */
    private class DoRequestUpdates extends TimerTask 
    {
	public void run() {
	    boolean cancelThis = true;
	    if (currentSearch != null) {
		ListIterator workersItr = currentSearch.getWorkers().listIterator();
		while(workersItr.hasNext()) {
		    SearchWorker worker = (SearchWorker)(workersItr.next());
		    switch(worker.getStatus()) {
		    case SearchWorker.READY:
		    case SearchWorker.RUNNING:
			worker.setUpdateNeeded(true);
			cancelThis = false;
		    }
		}
	    }
	    if (cancelThis) {
		cancel();
	    }
	}
    }


    /** 
     * Replace the standard event queue with one that suspends 
     * GUI updates when a drag gesture begins and resumes updates
     * when the drag gesture ends.
     */
    private class AppEventQueue extends EventQueue {
	protected void dispatchEvent(AWTEvent event) {
	    switch(event.getID()) {
	    case MouseEvent.MOUSE_PRESSED:
		suspendUpdates();
		super.dispatchEvent(event);
		break;
	    case MouseEvent.MOUSE_RELEASED:
		super.dispatchEvent(event);
		resumeUpdates();
		break;
	    default:
		super.dispatchEvent(event);
	    }
	}
    }


    private abstract class Command extends AbstractAction {
	Command(String title, String tooltip) {
	    super(title);
	    putValue("tooltip", tooltip);
	}
    }


    private final Action startSearchAction = new Command(
        "Search", "Start a web search")
    {
	public void actionPerformed(ActionEvent e) {
	    startSearch();
	}
    };


    private final Action stopSearchAction = new Command(
        "Stop", "Stop the current web search")
    {
	public void actionPerformed(ActionEvent e) {
	    stopSearch(currentSearch);
	}
    };


    private final Action showHitsAction = new Command(
        "Show Search Hits", "Show the search hits for the current search")
    {
	public void actionPerformed(ActionEvent e) {
	    showHits(currentSearch);
	}
    };


    private final Action showWorkersAction = new Command(
        "Show Workers", "Show the worker threads for the curtent search")
    {
	public void actionPerformed(ActionEvent e) {
	    showWorkers(currentSearch);
	}
    };


    private final Action quitAction = new Command(
       "Quit", "Exit the Application") 
    {
	public void actionPerformed(ActionEvent e) {
	    quit();
	}
    };
}
